Pro ASP.NET Core MVC2(第7版)翻译

第28章:初识 Identity

作者:Adam Freeman
翻译:陈广
日期:2018-10-27


ASP.NET Core Identity 是来自 Microsoft 的 API,用于管理 ASP.NET 应用程序中的用户。在本章中,我将演示如何设置ASP.NET Core Identity 并创建一个简单的用户管理工具来管理存储在数据库中的单个用户帐户。

ASP.NET Core Identity 支持其他类型的用户帐户,例如那些使用活动目录存储的用户帐户,但我不对它们进行描述,因为它们并不是通常在公司之外使用的用户帐户(在这些用户帐户中,活动指令实现往往非常复杂,因此我很难提供有用的一般示例)。

译者注:Identity 应译为身份,Visual Studio 中文版也是使用“身份”。但如果在文章里这样翻译,如 “ASP.NET Core 身份”,总会让人感觉怪怪的。另外很多有关身份验证的第三方组件都会带 Identity 这个单词。想来想去,还是直接用原文吧。

注意:本章要求为 Vistual Studio 安装 SQL Server LocalDB 功能。您可以通过运行 Visual Studio 安装程序并 选择 SQL Server Express 2016 LocalDB 选项来添加 LocalDB。

在第29章中,我将向您展示如何使用这些用户帐户执行身份验证和授权,在第30章中,我将向您展示如何超越基础知识并应用一些高级技术。表28-1为 ASP.NET Core Identity 简历。

表 28-1:ASP.NET Core Identity 简历

问题 回答
它是什么? ASP.NET Core Identity 是一种API,用于通过 Entity Framework Core 管理用户并将用户数据存储在存储库(如关系数据库)中。
它有何用途? 用户管理是大多数应用程序的一个重要特性,ASP.NET Core Identity 提供了一个现成的、经过良好测试的平台,不需要您创建常见需求函数的自定义版本。
如何使用它? Identity 通过添加到Startup类的服务和中间件来使用,它充当了应用程序和 Identity 功能之间桥梁。
是否有任何缺陷或限制? Microsoft 已经弥补了早期 ASP.NET 用户管理 API 的不灵活之处,因为它使 Identity 变得如此灵活和可配置,因此它可能是一个挑战,无法确定什么是可能的,什么是您需要的。在这本书中,我只触及了深奥而复杂的系统的表面。
有没有其他选择? 您可以实现您自己的API,但这可能需要做很多工作,如果不小心执行,就会产生安全漏洞。

表 28-2:本章摘要

问题 解决方案 清单
将 Identity 加入项目 添加 ASP.NET Identity 和 Entity Framework Core 的中间件,创建用户类和数据库 context 类,并创建数据库迁移 1-13
读取用户数据 使用 context 类查询 Identity 数据库 14-15
创建用户帐户 调用UserManager.CreateAsync方法 16-18
更改默认密码策略 Startup类中设置密码选项 19
实现自定义密码验证 实现IPasswordValidator接口或实现PasswordValidator类的子类 20-22
更改帐户验证策略 Startup类中设置用户选项 23
实现自定义帐户验证 实现IUserValidator接口或实现UserValidator类的子类 24-26
删除用户帐户 调用UserManager.DeleteAsync方法 27、28
编辑用户帐户 调用UserManager.UpdateAsync方法 29-31

准备示例项目

在本章中,我使用【ASP.NET Core Web 应用程序(.NET core)】模板创建了一个名为 Users 的新的空项目。此示例应用程序需要 Entity Framework Core 命令行工具,需要在【解决方案资源管理器】的 Usres 项目上单击鼠标右键,并选择【编辑 Users.csproj】,并通过手工的方式添加元素,如清单28-1所示。

清单 28-1:Users 文件夹下的 Users.csproj 文件,添加程序包

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0 " />
  </ItemGroup>

</Project>

清单28-2显示了Startup类,它设置了 MVC 框架,并启用了对开发有用的中间件组件,如第14章所述。

清单 28-2:Users 文件夹下的 Startup.cs 文件的内容

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Users
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

创建控制器和视图

我创建了 Controllers 文件夹,添加了一个名为 HomeController.cs 的类文件,并定义了如清单28-3所示的控制器。我将使用这个控制器来描述用户帐户和数据的详细信息,Index action 方法通过View方法将值字典传递给默认视图。

清单 28-3:Controllers 文件夹下的 HomeController.cs 文件的内容

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace Users.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() =>
            View(new Dictionary<string, object>
                { ["Placeholder"] = "Placeholder" });
    }
}

为了向控制器提供一个视图,我创建了 Views/Home 文件夹,并添加了一个名为 Index.cshtml 的视图文件,其标记如清单28-4所示。

清单 28-4:Views/Home 文件夹下的 Index.cshtml 文件的内容

@model Dictionary<string, object>

<div class="bg-primary m-1 p-1 text-white"><h4>User Details</h4></div>

<table class="table table-sm table-bordered m-1 p-1">
    @foreach (var kvp in Model)
    {
        <tr><th>@kvp.Key</th><td>@kvp.Value</td></tr>
    }
</table>

视图在表中显示模型字典的内容。为了支持视图,我创建了 Views/Shared 文件夹,添加了一个名为 _Layout.cshtml 的视图文件,其标记如清单28-5所示。

清单 28-5:Views/Shared 文件夹下的 _Layout.cshtml 文件的内容

<!DOCTYPE html>
<html>
<head>
    <title>Users</title>
    <meta name="viewport" content="width=device-width" />
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    @RenderBody()
</body>
</html>

视图依赖于 Bootstrap CSS 包来设置 HTML 元素的样式。要将 Bootstrap 添加到项目中,我在 Users 项目中单击鼠标右键,在弹出菜单中选择【添加】➤【添加客户端库】,并将 twitter-bootstrap 添加至项目中。最终生成的 libman.json 配置文件代码清单27-6所示: 清单 28-6:Users 文件夹下的 libman.json 文件,添加包

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    }
  ]
}

最后的准备工作是在 Views 文件夹中创建 _ViewImports.cshtml 文件,该文件设置内置标签助手,以便在视图中使用,如清单28-7所示。

清单 28-7:Views 文件夹下的 _ViewImports.cshtml 文件的内容

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

最后一个添加是在 Views 文件夹中创建一个名为 _ViewStart.cshtml 的视图开始文件,其内容如清单28-8所示。这将确保我在清单28-7中创建的布局将被应用程序中的所有视图所使用。

清单 28-8:Views 文件夹下的 _ViewStart.cshtml 文件的内容

@{
    Layout = "_Layout";
}

启动应用程序,您将看到如图28-1所示的输出。

图28-1 运行示例应用程序

设置 ASP.NET Core Identity

设置 Identity 的过程涉及到应用程序的几乎所有部分,需要新的模型类、配置更改以及控制器和 action 来支持身份验证和授权操作。在接下来的部分中,我将介绍在基本配置中设置 Identity 的过程,以显示所涉及的不同步骤。在应用程序中使用 Identity 有很多不同的方法,我在本章中使用的配置遵循最简单和最常用的选项。

创建用户类

第一步是定义一个类来表示应用程序中的用户,这个类被称为用户类,用户类是从Microsoft.AspNetCore.Identity命名空间中定义的IdentityUser派生的。IdentityUser提供了基本的用户表示,它可以通过向派生类添加属性来扩展,我在第30章中对此进行了描述。表28-3显示了IdentityUser定义的最有用的内置属性,包括我在本章中使用的属性。

表 28-3:IdentityUser类定义的属性

名称 描述
Id 此属性包含用户的惟一 ID
UserName 此属性返回用户的用户名
Claims 此属性返回用户的声明集合,我在第30章中对此进行了描述。
Email 此属性包含用户的 e-mail 地址
Logins 此属性返回用户的登录集合,用于第三方身份验证,如第30章所述。
PasswordHash 此属性返回用户密码的哈希形式,我在《实现编辑功能》一节中使用该格式。
Roles 此属性返回用户所属的角色集合,我在第29章中对此进行了描述。
PhoneNumber 此属性返回用户的电话号码
SecurityStamp 此属性返回在更改用户 Identity 时更改的值,如密码更改。

单个属性目前并不重要。重要的是,IdentityUser类提供对用户的基本信息的访问:用户名、电子邮件、电话号码、密码哈希、角色成员身份等等。如果我想存储有关用户的任何其他信息,必须向从IdentityUser派生的类中添加属性,这些属性将用于表示我的应用程序中的用户。

为了为应用程序创建用户类,我创建了 Models 文件夹,并添加了一个名为 AppUserModels.cs 的类文件,用于创建AppUser类,如清单28-9所示。

清单 28-9:Models 文件夹下的 AppUser.cs 文件的内容

using Microsoft.AspNetCore.Identity;

namespace Users.Models
{
    public class AppUser : IdentityUser
    {
        // 基本 Identity 安装无需其它成员
    }
}

这是我现在要做的全部工作,尽管我会在第30章中回到这个类,到时将向您展示如何添加特定于应用程序的用户数据属性。

配置视图导入

虽然与设置 ASP.NET Core Identity 无关,但在下一节中,我将在视图中使用AppUser对象。为了简化视图的编写,我将Users.Models命名空间添加到视图导入文件中,如清单28-10所示。

清单 28-10:Views 文件夹下的 _ViewImports.cshtml 文件,添加命名空间

@using Users.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

创建数据库 Context 类

下一步是创建一个在AppUser类上操作的 Entity Framework Core 数据库 context 类。context 类是从IdentityDbContext<T>派生的,其中T是用户类(示例项目中的AppUser)。我在 Models 文件夹中添加了一个名为 AppIdentityDbContext.cs 的类文件,并定义了清单28-11所示的类。

清单 28-11:Models 文件夹下的 AppIdentityDbContext.cs 文件的内容

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Users.Models
{
    public class AppIdentityDbContext : IdentityDbContext<AppUser>
    {
        public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options)
            : base(options) { }
    }
}

可以扩展数据库 context 类以定制数据库的设置和使用方式,但是对于一个基本的 ASP.NET Core Identity 应用程序来说,仅仅定义类就足够开始并为以后的任何定制提供占位符。

注意:如果这些类的角色没有意义,不要担心。如果您不熟悉 Entity Framework Core,那么我建议您将其视为黑匣子。一旦基本的构建块就位 —— 您可以将这些构建块复制到您的项目中以使其正常工作 —— 那么您就很少需要对它们进行编辑了。

配置数据库连接字符串设置

ASP.NET Core Identity 的第一个配置步骤是定义将用于数据库的连接字符串。惯例是将连接字符串放入 appsettings.json 文件中,然后在应用程序启动时加载连接字符串,并可以在Startup类中访问,如第14章所述。我使用 ASP.NET 【应用设置文件】模板在项目的根文件夹中创建 appsettings.json 文件,并添加了如清单28-12所示的配置设置。

清单 28-12:Users 文件夹下的 appsettings.json 文件的内容

{
  "Data": {
    "SportStoreIdentity": {
      "ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=IdentityUsers; Trusted_Connection=True; MultipleActiveResultSets=true"
    }
  }
}

在连接字符串中,我指定了localdb选项,它为开发者提供了方便的数据库支持。对于数据库本身,我指定了名称IdentityUsers

注意:打印页面的宽度不允许对连接字符串进行合理的格式化,连接字符串必须出现在一条未中断的行中。这在 Visual Studio 编辑器中运行良好,但意味着它必须在列表中包装多行。当您将连接字符串添加到您自己的项目时,请确保它位于单行上。

有了数据库连接字符串,我可以更新Startup类以接收配置数据,如清单28-13所示。

清单 28-13:Users 文件夹下的 Startup.cs 文件,读取应用程序设置

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Users.Models;

namespace Users
{
    public class Startup
    {
        public Startup(IConfiguration configuration) =>
            Configuration = configuration;

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<AppIdentityDbContext>(options =>
                options.UseSqlServer(
                    Configuration["Data:SportStoreIdentity:ConnectionString"]));

            services.AddIdentity<AppUser, IdentityRole>()
                .AddEntityFrameworkStores<AppIdentityDbContext>()
                .AddDefaultTokenProviders();

            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseAuthentication();
            app.UseMvcWithDefaultRoute();
        }
    }
}

创建一个基本的 ASP.NET Core Identity 安装需要三组更改。第一步是设置 Entity Framework (EF) Core,它为 MVC 应用程序提供数据访问服务。

...
services.AddDbContext<AppIdentityDbContext>(options =>
    options.UseSqlServer(Configuration["Data:SportStoreIdentity:ConnectionString"]));
...

AddDbContext方法添加 Entity Framework Core 所需的服务,UseSqlServer方法设置使用 Microsoft SQL Server 存储数据所需的支持。AddDbContext方法允许我应用我在前面创建的数据库 context 类,并指定它将被备份到一个 SQL Server 数据库中,该数据库的连接字符串是从应用程序的配置中获得的(在示例应用程序中,该配置为 appsettings.json 文件)。

我还需要设置 ASP.NET Core Identity 的服务,如下所示:

...
services.AddIdentity<AppUser, IdentityRole>()
    .AddEntityFrameworkStores<AppIdentityDbContext>()
    .AddDefaultTokenProviders();
...

AddIdentity方法具有类型参数,这些参数指定用于表示用户的类和用于表示角色的类。我已经为用户指定了AppUser类,为角色指定了内置的IdentityRole类。AddEntityFrameworkStores方法指定 Identity 应该使用 Entity Framework Core 来存储和检索其数据,使用我先前创建的数据库 context 类。AddDefaultTokenProviders方法使用默认配置来支持需要令牌的操作,例如更改密码。

Startup类的最后更改将 ASP.NET Core Identity 添加到请求处理管道中,该管道允许用户凭据与基于 cookie 或 URL 重写的请求相关联,这意味着用户帐户的详细信息不直接包含在发送给应用程序的 HTTP 请求或它生成的响应中。

...
app.UseAuthentication();
...

创建标识数据库

几乎一切就绪,剩下的唯一步骤就是实际创建用于存储 Identity 数据的数据库。打开一个新的命令提示符或 PowerShell 窗口,导航到 Users 项目文件夹(包含startup.cs文件的文件夹),并运行以下命令:

dotnet ef migrations add Initial

正如我在为 SportsStore 应用程序设置数据库时解释的那样,Entity Framework Core 通过一个名为迁移的特性来管理对数据库架构的更改。修改用于生成架构的模型类时,可以生成包含更新数据库所需的 SQL 命令的迁移文件。该命令创建将为 Identity 设置数据库的迁移文件。

dotnet ef命令完成时,您将在【解决方案资源管理器】中看到一个 Migrations 文件夹。如果检查文件的内容,您可以看到用于创建初始数据库的 SQL 命令。要使用迁移文件创建数据库,请运行以下命令:

dotnet ef database update

该过程可能需要一段时间才能完成,但是一旦命令完成,数据库将被创建好并准备使用。

使用 ASP.NET Core Identity

现在基本设置已经完成,我可以开始使用 ASP.NET Core Identity 将对管理用户的支持添加到示例应用程序中。在下面的部分中,我将演示如何使用 Identity API 创建允许对用户进行集中管理的管理工具。

集中式用户管理工具在几乎所有应用程序中都很有用,即使是允许用户创建和管理自己帐户的应用程序也是如此。例如,总会有一些客户需要创建大量帐户,并支持需要检查和调整用户数据的问题。从本章的角度来看,管理工具是有用的,因为它们将许多基本用户管理功能合并到少数类中,从而使它们成为演示 ASP.NET Core Identity 的基本特性的有用示例。

枚举用户帐户

本节的出发点是枚举数据库中的所有用户帐户,这将允许我看到以后添加到应用程序中的代码的效果。我首先在 Controllers 文件夹中添加一个名为 AdminController.cs 的类文件,并使用它来定义如清单28-14所示的控制器,我将使用它来定义我的用户管理功能。

清单 28-14:Controllers 文件夹下的 AdminController.cs 文件的内容

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Users.Models;

namespace Users.Controllers
{
    public class AdminController : Controller
    {
        private UserManager<AppUser> userManager;

        public AdminController(UserManager<AppUser> usrMgr)
        {
            userManager = usrMgr;
        }

        public ViewResult Index() => View(userManager.Users);
    }
}

Index action 方法枚举由 Identity 系统管理的用户;当然,目前没有任何用户,但很快就会出现。对用户数据的访问是通过由控制器构造函数接收并通过依赖注入提供的UserManager<AppUser>对象进行的。

使用UserManager<AppUser>对象,我可以开始查询数据存储。Users属性返回用户对象的枚举 —— 此应用程序中AppUser类的实例 —— 可以使用 LINQ 查询和操作。在 action 方法中,我将Users属性的值(它将枚举数据库中的所有用户)传递给视图方法,以便显示帐户详细信息。为了向 action 方法提供一个视图,我创建了 Views/Admin 文件夹,向其添加了一个名为 Index.cshtml 的文件,并应用了清单28-15中所示的标记。

清单 28-15:Views/Admin 文件夹下的 Index.cshtml 文件的内容

@model IEnumerable<AppUser>

<div class="bg-primary m-1 p-1 text-white"><h4>User Accounts</h4></div>

<table class="table table-sm table-bordered">
    <tr><th>ID</th><th>Name</th><th>Email</th></tr>
    @if (Model.Count() == 0)
    {
        <tr><td colspan="3" class="text-center">No User Accounts</td></tr>
    }
    else
    {
        foreach (AppUser user in Model)
        {
            <tr>
                <td>@user.Id</td>
                <td>@user.UserName</td>
                <td>@user.Email</td>
            </tr>
        }
    }
</table>

<a class="btn btn-primary" asp-action="Create">Create</a>

此视图包含一个表,每一行代表一个用户,其中包含唯一ID、用户名和 e-mail 地址的列。如果数据库中没有用户,则会显示一条消息,如图28-2所示,如果启动应用程序并请求 /Admin URL,就可以看到它。

图28-2 显示用户列表(空)

我在视图中包含了一个Create anchor 元素链接(样式为一个按钮),该链接的目标是Admin控制器上的Create操作。这将是支持添加用户的 action。


重置数据库

通过打开 Visual Studio 的【SQL Server 对象资源管理器】窗口,可以看到为 Identity 而创建的数据库。如果这是您第一次使用【SQL Server 对象资源管理器】窗口,则需要从【工具】菜单中选择【连接到数据库】以告诉 Visual Studio 您正在使用的数据库。对于数据源,选择【Microsoft SQL Server】,使用【(localdb)\mssqllocaldb】作为服务器名称,保持选中【Windows 身份验证】,然后单击【选择或输入数据库名称】字段的下拉箭头。几秒钟后,您将看到可用的 LocalDB 数据库的列表,您应该能够选择IdentityUsers,这是示例应用程序的数据库。单击【确定】,一个新条目将出现在【SQL Server 对象资源管理器】窗口中。Visual Studio 将记住数据库,因此您只需要执行一次此过程。

您可以通过在【SQL Server 对象资源管理器】窗口中展开【(localdb)\mssqllocaldb】➤【数据库】➤【IdentityUsers】项来查看数据库。您将能够看到由迁移文件创建的表,名称为 AspNetUsers 和 AspNetRoles。一旦将用户添加到数据库中,就可以查询数据库以查看表的内容,我在下一节中将对此进行演示。

要删除数据库,右键单击【SQL Server 对象资源管理器】窗口中的IdentityUsers项,然后从弹出菜单中选择【删除】。选中删除数据库对话框中的两个选项,然后单击【确定】按钮删除数据库。

若要重新创建数据库,请打开【程序包管理器控制台】窗口并运行以下命令:

dotnet ef database update

数据库将被重新创建,并准备好在您下次启动应用程序时使用。


创建用户

我将对应用程序接收到的输入使用 MVC 模型验证,最简单的方法是为控制器支持的每个 action 创建简单的视图模型。我在 Models 文件夹中添加了一个名为 UserViewModels.cs 的类文件,并使用它来定义清单28-16所示的类。

清单 28-16:Models 文件夹下的 UserViewModels.cs 文件的内容

using System.ComponentModel.DataAnnotations;

namespace Users.Models
{
    public class CreateModel
    {
        [Required]
        public string Name { get; set; }
        [Required]
        public string Email { get; set; }
        [Required]
        public string Password { get; set; }
    }
}

我定义的初始模型称为CreateModel,它定义了创建用户帐户所需的基本属性:用户名、电子邮件地址和密码。我使用了System.ComponentModel.DataAnnotations命名空间中的Required特性来表示模型中定义的所有三个属性都需要值。

在清单28-17中,我向Admin控制器添加了一对Create action 方法;它们是上一节Index视图中的链接所指向的,并使用标准控制器模式向用户显示 GET 请求的视图,并为 POST 请求处理表单数据。

清单 28-17:Controllers 文件夹下的 AdminController.cs 文件,定义 Create Action

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Users.Models;
using System.Threading.Tasks;

namespace Users.Controllers
{
    public class AdminController : Controller
    {
        private UserManager<AppUser> userManager;

        public AdminController(UserManager<AppUser> usrMgr)
        {
            userManager = usrMgr;
        }

        public ViewResult Index() => View(userManager.Users);

        public ViewResult Create() => View();

        [HttpPost]
        public async Task<IActionResult> Create(CreateModel model)
        {
            if (ModelState.IsValid)
            {
                AppUser user = new AppUser
                {
                    UserName = model.Name,
                    Email = model.Email
                };
                IdentityResult result
                    = await userManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    return RedirectToAction("Index");
                }
                else
                {
                    foreach (IdentityError error in result.Errors)
                    {
                        ModelState.AddModelError("", error.Description);
                    }
                }
            }
            return View(model);
        }
    }
}

此清单的重要部分是Create action 方法,该方法接受CreateModel参数,并且在管理员提交表单数据时将被调用。ModelState.IsValid属性用于检查数据是否包含所需的值,如果包含,将创建AppUser类的一个新实例,并将其传递给异步UserManager.CreateAsync方法,如下所示:

...
AppUser user = new AppUser { UserName = model.Name, Email = model.Email };
IdentityResult result = await userManager.CreateAsync(user, model.Password);
...

CreateAsync方法的结果是一个IdentityResult对象,它通过表28-4中列出的属性描述操作的结果。

表 28-4:IdentityResult 类定义的属性

名称 描述
Succeeded 如果操作成功,则返回true
Errors 在尝试操作遭遇错误时,返回描述错误的IdentityError对象序列。

我检查Create action 方法中的Succeeded属性,以确定是否在数据库中创建了新的用户记录。如果Succeeded属性为true,则将客户端重定向到Index action,以便显示用户列表。

...
if (result.Succeeded) {
    return RedirectToAction("Index");
} else {
    foreach (IdentityError error in result.Errors) {
        ModelState.AddModelError("", error.Description);
    }
}

如果Succeeded的属性为false,则枚举Errors属性提供的IdentityError对象序列,并使用ModelState.AddModelError方法的Description属性创建模型级别验证错误,如第27章所述。

为了提供带有视图的新 action 方法,我在 Views/Admin 文件夹中创建了一个名为 Create.cshtml 的视图文件,并添加了清单28-18所示的标记。

清单 28-18:Views/Admin 文件夹下的 Create.cshtml 文件的内容

@model CreateModel

<div class="bg-primary m-1 p-1 text-white"><h4>Create User</h4></div>
<div asp-validation-summary=" All" class="text-danger"></div>
<form asp-action="Create" method="post">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input asp-for="Name" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="Email"></label>
        <input asp-for="Email" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="Password"></label>
        <input asp-for="Password" class="form-control" />
    </div>
    <button type="submit" class="btn btn-primary">Create</button>
    <a asp-action="Index" class="btn btn-secondary">Cancel</a>
</form>

这个视图没有什么特别之处 —— 它是一种简单的形式,它收集 MVC 将绑定到传递给Create action 方法的模型类的属性的值,并包含一个用于验证错误时的摘要。

测试 Create 功能

要测试创建新用户帐户的能力,启动应用程序,导航到 /Admin URL,然后单击【Create】按钮。用表28-5中所示的值填写表单。

提示:有些域名保留用于测试,包括 example.com。您可以在 https://tools.ietf.org/html/rfc2606 看到一个完整的列表。

表 28-5:创建示例用户的值

名称
Name Joe
Email joe@example.com
Password Secret123$

输入这些值后,单击【Create】按钮。ASP.NET Core Identity 将创建用户帐户,当您的浏览器被重定向到Index action 方法时将显示该帐户,如图28-3所示(您将看到不同的 ID 值,因为每个用户帐户的 ID 都是随机生成的)。

图28-3 添加新用户帐户

再次单击【Create】按钮,并使用表28-5中的值在表单中输入相同的详细信息。这一次,当您提交表单时,您将看到通过模型验证摘要报告的一个错误,如图28-4所示。

图28-4 尝试创建新用户时出错

验证密码

最常见的要求之一,尤其是对于公司应用程序,是强制执行密码策略。通过运行应用程序可以看到默认策略,请求 /Admin/Create URL,并使用表28-6所示的数据填充表单,其中与前一节中的数据的重要区别是输入到密码字段中的值。

表 28-6:创建示例用户的值

名称
Name Alice
Email alice@example.com
Password secret

当您向服务器提交表单时,Identity 系统检查候选密码,如果不符合要求,则生成错误,生成如图28-5所示的错误。

图28-5 密码验证错误

您可以在Startup类中配置密码验证规则,如清单28-19所示。

清单 28-19:Users 文件夹中的 Startup.cs 文件,配置密码验证

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Users.Models;

namespace Users
{
    public class Startup
    {
        public Startup(IConfiguration configuration) =>
            Configuration = configuration;

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<AppIdentityDbContext>(options =>
                options.UseSqlServer(
                    Configuration["Data:SportStoreIdentity:ConnectionString"]));

            services.AddIdentity<AppUser, IdentityRole>(opts => {
                opts.Password.RequiredLength = 6;
                opts.Password.RequireNonAlphanumeric = false;
                opts.Password.RequireLowercase = false;
                opts.Password.RequireUppercase = false;
                opts.Password.RequireDigit = false;
            }).AddEntityFrameworkStores<AppIdentityDbContext>()
                .AddDefaultTokenProviders();

            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseAuthentication();
            app.UseMvcWithDefaultRoute();
        }
    }
}

AddIdentity方法可以与接受IdentityOptions对象的函数一起使用,该对象的Password属性返回PasswordOptions类的实例,该实例提供表28-7中描述的用于管理密码策略的属性。

表 28-7:PasswordOptions 属性

名称 描述
RequiredLength int属性用于指定密码的最小长度
RequireNonAlphanumeric 将此bool属性设置为true要求密码至少包含一个非字母或数字的字符。
RequireLowercase 将此bool属性设置为true要求密码至少包含一个小写字符。
RequireUppercase 将此bool属性设置为true要求密码至少包含一个大写字符。
RequireDigit 将此bool属性设置为true需要密码至少包含数字字符。

在清单中,我指定密码必须最小长度为6个字符,并禁用其他约束。在一个实际的项目中,这不是你应该轻易做的事情,但是它也是一个有效的演示。如果启动应用程序,导航到 /Admin/Create URL,并重复表单提交,您将看到密码秘密现在已被接受,并且已经为 Alice 创建了一个新帐户,如图28-6所示。

图28-6 更改密码验证策略

实现自定义密码验证器

对于大多数应用程序来说,内置密码验证已经足够了,但是您可能需要实现自定义策略,特别是当您正在实现复杂密码策略常见的企业业务线应用程序时。密码验证功能由Microsoft.AspNetCore.Identity命名空间下的IPasswordValidator<T>接口定义,其中的T为应用程序指定用户类(本例为AppUser)。

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Identity
{
    public interface IPasswordValidator<TUser> where TUser : class 
    {
        Task<IdentityResult> ValidateAsync(UserManager<TUser> manager,
            TUser user, string password);
    }
}

为了验证密码,会调用ValidateAsync方法,并通过UserManager对象(它允许查询 Identity 数据库)、代表用户的对象和候选密码提供 context 数据。结果是一个IdentityResult对象,如果不存在验证问题,则使用静态Success属性创建;或者使用静态Failed方法创建,该方法传递一个IdentityError对象数组,每个对象都描述一个验证问题。

为了演示自定义验证策略的使用,我创建了 Infrastructure 文件夹,在其中添加了一个名为 CustomPasswordValidator.cs 的类文件,并使用该文件定义了清单28-20中所示的类。

清单 28-20:Infrastructure 文件夹下的 CustomPasswordValidator.cs 文件的内容

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Users.Models;

namespace Users.Infrastructure
{
    public class CustomPasswordValidator : IPasswordValidator<AppUser>
    {
        public Task<IdentityResult> ValidateAsync(UserManager<AppUser> manager,
        AppUser user, string password)
        {
            List<IdentityError> errors = new List<IdentityError>();

            if (password.ToLower().Contains(user.UserName.ToLower()))
            {
                errors.Add(new IdentityError
                {
                    Code = "PasswordContainsUserName",
                    Description = "Password cannot contain username"
                });
            }
            if (password.Contains("12345"))
            {
                errors.Add(new IdentityError
                {
                    Code = "PasswordContainsSequence",
                    Description = "Password cannot contain numeric sequence"
                });
            }
            return Task.FromResult(errors.Count == 0 ?
                IdentityResult.Success : IdentityResult.Failed(errors.ToArray()));
        }
    }
}

验证器类检查密码,保证其不包含用户名,及序列12345。在清单28-21中,我已经将CustomPasswordValidator类注册为AppUser对象的密码验证器。

清单 28-21:Startup.cs 文件,注册自定义密码验证器

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Users.Models;
using Users.Infrastructure;

namespace Users
{
    public class Startup
    {
        public Startup(IConfiguration configuration) =>
            Configuration = configuration;

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IPasswordValidator<AppUser>,
                CustomPasswordValidator>();

            services.AddDbContext<AppIdentityDbContext>(options =>
                options.UseSqlServer(
                    Configuration["Data:SportStoreIdentity:ConnectionString"]));

            services.AddIdentity<AppUser, IdentityRole>(opts => {
                opts.Password.RequiredLength = 6;
                opts.Password.RequireNonAlphanumeric = false;
                opts.Password.RequireLowercase = false;
                opts.Password.RequireUppercase = false;
                opts.Password.RequireDigit = false;
            }).AddEntityFrameworkStores<AppIdentityDbContext>()
                .AddDefaultTokenProviders();

            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseAuthentication();
            app.UseMvcWithDefaultRoute();
        }
    }
}

要测试自定义策略,启动应用程序,请求 /Admin/Create URL,并使用表28-8中的数据值填写表单。

表 28-8:创建示例用户的值

名称
Name Bob
Emain bob@example.com
Password bob12345

表中的密码破坏了自定义类强制执行的验证规则,并导致了如图28-7所示的错误消息。

图28-7 使用自定义密码验证器

还可以在默认情况下使用的内置类所提供的基础上实现自定义验证策略。默认的类称为PasswordValidator,并在Microsoft.AspNetCore.Identity命名空间中定义。在清单28-22中,我已经更改了自定义验证器类,以便它是从PasswordValidator派生出来的,并且构建在它提供的基本检查之上。

清单 28-22:CustomPasswordValidator.cs 文件,从内置验证器派生

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Users.Models;
using System.Linq;

namespace Users.Infrastructure
{
    public class CustomPasswordValidator : PasswordValidator<AppUser>
    {
        public override async Task<IdentityResult> ValidateAsync(
            UserManager<AppUser> manager, AppUser user, string password)
        {
            IdentityResult result = await base.ValidateAsync(manager,
                user, password);
            List<IdentityError> errors = result.Succeeded ?
                new List<IdentityError>() : result.Errors.ToList();
            if (password.ToLower().Contains(user.UserName.ToLower()))
            {
                errors.Add(new IdentityError
                {
                    Code = "PasswordContainsUserName",
                    Description = "Password cannot contain username"
                });
            }
            if (password.Contains("12345"))
            {
                errors.Add(new IdentityError
                {
                    Code = "PasswordContainsSequence",
                    Description = "Password cannot contain numeric sequence"
                });
            }
            return errors.Count == 0 ? IdentityResult.Success
                : IdentityResult.Failed(errors.ToArray());
        }
    }
}

要测试组合验证,请运行应用程序,并使用表28-9中的数据填充 /Admin/Create URL 返回的表单。

表 28-9:创建示例用户的值

名称
Name Bob
Email bob@example.com
Password 12345

提交表单时,您将看到自定义验证错误和内置验证错误的组合,如图28-8所示。

图28-8 使用自定义密码验证器

验证用户详细信息

在创建帐户时,还会对用户名和电子邮件地址执行验证。要查看内置验证,启动应用程序并使用表28-10中所示的数据填写 /Admin/Create 表单。

表 28-10:创建示例用户的值

名称
Name Bob!
Email alice@example.com
Password secret

当提交表单时,将看到图28-9所示的验证错误。

图28-9 用户帐户验证错误

可以使用IdentityOptions.User属性在Startup类中配置验证,该属性返回UserOptions类的实例。表28-11描述了UserOptions属性。

表 28-11:UserOptions 属性

名称 描述
AllowedUserNameCharacters 此字符串属性包含可以在用户名中使用的所有合法字符。默认值指定 a-z、A-Z 和 0-9 以及连字符、句点、下划线和@字符。此属性不是正则表达式,必须在字符串中显式指定每个合法字符。
RequireUniqueEmail 将此bool属性设置为true需要新帐户指定以前未使用的电子邮件地址。

在清单28-23中,我更改了应用程序的配置,以便需要唯一的电子邮件地址,并且用户名中只允许小写字母字符。

清单 28-23:Startup.cs 文件,更改用户帐户验证设置

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Users.Models;
using Users.Infrastructure;

namespace Users
{
    public class Startup
    {
        public Startup(IConfiguration configuration) =>
            Configuration = configuration;

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IPasswordValidator<AppUser>,
                CustomPasswordValidator>();

            services.AddDbContext<AppIdentityDbContext>(options =>
                options.UseSqlServer(
                    Configuration["Data:SportStoreIdentity:ConnectionString"]));

            services.AddIdentity<AppUser, IdentityRole>(opts => {
                opts.User.RequireUniqueEmail = true;
                opts.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz";
                opts.Password.RequiredLength = 6;
                opts.Password.RequireNonAlphanumeric = false;
                opts.Password.RequireLowercase = false;
                opts.Password.RequireUppercase = false;
                opts.Password.RequireDigit = false;
            }).AddEntityFrameworkStores<AppIdentityDbContext>()
                .AddDefaultTokenProviders();

            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseAuthentication();
            app.UseMvcWithDefaultRoute();
        }
    }
}

如果您重新提交上一次测试中的数据,将看到电子邮件地址现在会导致一个错误,并且名称中使用的字符仍然被拒绝,如图28-10所示。

图28-10 更改帐户验证设置

实现自定义用户验证

验证功能由IUserValidator<T>接口指定,该接口在Microsoft.AspNetCore.Identity命名空间中定义。

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Identity 
{
    public interface IUserValidator<TUser> where TUser : class 
    {
        Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user);
    }
}

调用ValidateAsync方法用于执行验证,并使用IdentityResult对象返回结果,该对象与验证密码的类相同。为了演示自定义验证器,我在 Infrastructure 文件夹中添加了一个名为 CustomUserValidator.cs 的类,并使用它来定义清单28-24所示的类。

清单 28-24:Infrastructure 文件夹下的 CustomUserValidator.cs 文件的内容

using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Users.Models;

namespace Users.Infrastructure
{
    public class CustomUserValidator : IUserValidator<AppUser>
    {
        public Task<IdentityResult> ValidateAsync(UserManager<AppUser> manager,
        AppUser user)
        {
            if (user.Email.ToLower().EndsWith("@example.com"))
            {
                return Task.FromResult(IdentityResult.Success);
            }
            else
            {
                return Task.FromResult(IdentityResult.Failed(new IdentityError
                {
                    Code = "EmailDomainError",
                    Description = "Only example.com email addresses are allowed"
                }));
            }
        }
    }
}

这个验证器检查电子邮件地址的域名,以确保它是 example.com 域名的一部分。在清单28-25中,我已经将自定义类注册为AppUser对象的验证器。

清单 28-25:Startup.cs 文件,注册客户用户验证器

...
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IPasswordValidator<AppUser>,
        CustomPasswordValidator>();
    services.AddTransient<IUserValidator<AppUser>,
        CustomUserValidator>();

    services.AddDbContext<AppIdentityDbContext>(options =>
        options.UseSqlServer(
            Configuration["Data:SportStoreIdentity:ConnectionString"]));

    services.AddIdentity<AppUser, IdentityRole>(opts => {
        opts.User.RequireUniqueEmail = true;
        opts.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz";
        opts.Password.RequiredLength = 6;
        opts.Password.RequireNonAlphanumeric = false;
        opts.Password.RequireLowercase = false;
        opts.Password.RequireUppercase = false;
        opts.Password.RequireDigit = false;
    }).AddEntityFrameworkStores<AppIdentityDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc();
}
...

要测试自定义验证器,请运行应用程序,并使用表28-12所示的数据填写 /Admin/Create 表单。

表 28-12:创建示例用户的值

名称
Name bob
Email bob@invalid.com
Password secret

用户名和密码通过验证,但电子邮件地址不在正确的域中。当您提交表单时,您将看到图28-11所示的验证错误。

图28-11 执行自定义用户验证

UserValidator<T>类提供的内置验证与自定义验证的组合过程与验证密码的模式相同,如清单28-26所示。

清单 28-26:CustomUserValidator.cs 文件,扩展内置用户验证

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Users.Models;

namespace Users.Infrastructure
{
    public class CustomUserValidator : UserValidator<AppUser>
    {
        public override async Task<IdentityResult> ValidateAsync(
        UserManager<AppUser> manager,
        AppUser user)
        {
            IdentityResult result = await base.ValidateAsync(manager, user);
            List<IdentityError> errors = result.Succeeded ?
                new List<IdentityError>() : result.Errors.ToList();
            if (!user.Email.ToLower().EndsWith("@example.com"))
            {
                errors.Add(new IdentityError
                {
                    Code = "EmailDomainError",
                    Description = "Only example.com email addresses are allowed"
                });
            }
            return errors.Count == 0 ? IdentityResult.Success
                : IdentityResult.Failed(errors.ToArray());
        }
    }
}

完成管理功能

我只需要实现编辑和删除用户的功能就可以完成我的管理工具。在清单28-27中,您可以看到我对 Views/Admin/Index.cshtml 文件所做的更改,以便在管理控制器中针对EditDelete action。

清单 28-27:Views/Admin 文件夹下的 Index.cshtml 文件,添加 Edit 和 Delete 按钮

@model IEnumerable<AppUser>

<div class="bg-primary m-1 p-1 text-white"><h4>User Accounts</h4></div>

<div class="text-danger" asp-validation-summary="ModelOnly"></div>

<table class="table table-sm table-bordered">
    <tr><th>ID</th><th>Name</th><th>Email</th></tr>
    @if (Model.Count() == 0)
    {
        <tr><td colspan="3" class="text-center">No User Accounts</td></tr>
    }
    else
    {
        foreach (AppUser user in Model)
        {
            <tr>
                <td>@user.Id</td>
                <td>@user.UserName</td>
                <td>@user.Email</td>
                <td>
                    <form asp-action="Delete" asp-route-id="@user.Id" method="post">
                        <a class="btn btn-sm btn-primary" asp-action="Edit"
                           asp-route-id="@user.Id">Edit</a>
                        <button type="submit"
                                class="btn btn-sm btn-danger">
                            Delete
                        </button>
                    </form>
                </td>
            </tr>
        }
    }
</table>
<a class="btn btn-primary" asp-action="Create">Create</a>

【Delete】按钮将表单发布到 Admin 控制器上的Delete action,这很重要,因为在更改应用程序状态时需要 POST 请求。【Edit】按钮是一个 anchor 元素,它将发送一个 GET 请求,因为编辑过程中的第一步是显示当前数据。【Edit】按钮包含在表单元素中,这样 Bootstrap CSS 样式就不会垂直堆叠起来。我还在视图中添加了一个模型验证摘要,这样我就可以轻松地显示来自其余管理功能的任何错误。

实现删除功能

UserManager<T>类定义了一个DeleteAsync方法,该方法接受用户类的一个实例,并将其从数据库中删除。在清单28-28中,您可以看到我是如何使用DeleteAsync方法实现 Admin 控制器的删除功能的。

清单 28-28:Controllers 文件夹下的 AdminController.cs 文件,删除用户

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Users.Models;
using System.Threading.Tasks;

namespace Users.Controllers
{
    public class AdminController : Controller
    {
        private UserManager<AppUser> userManager;

        public AdminController(UserManager<AppUser> usrMgr)
        {
            userManager = usrMgr;
        }

        // ...其它 actions 省略...

        [HttpPost]
        public async Task<IActionResult> Delete(string id)
        {
            AppUser user = await userManager.FindByIdAsync(id);
            if (user != null)
            {
                IdentityResult result = await userManager.DeleteAsync(user);
                if (result.Succeeded)
                {
                    return RedirectToAction("Index");
                }
                else
                {
                    AddErrorsFromResult(result);
                }
            }
            else
            {
                ModelState.AddModelError("", "User Not Found");
            }
            return View("Index", userManager.Users);
        }

        private void AddErrorsFromResult(IdentityResult result)
        {
            foreach (IdentityError error in result.Errors)
            {
                ModelState.AddModelError("", error.Description);
            }
        }
    }
}

我的 action 方法接收用户的唯一ID作为参数,我使用FindByIdAsync方法来定位相应的用户对象,以便将它传递给DeleteAsync方法。DeleteAsync方法的结果是一个IdentityResult,我处理该它的方式与前面示例中的处理方法相同,以确保向用户显示任何错误。您可以通过创建一个新用户,然后单击 Index 视图中显示在它旁边的【Delete】按钮来测试删除功能,如图28-12所示。

图28-12 删除用户帐户

实现编辑功能

要完成管理工具,我需要添加对编辑用户帐户的电子邮件地址和密码的支持。这些是用户目前定义的唯一属性,但我将在第30章中向您展示如何使用自定义属性扩展架构。清单28-29显示了我添加到 Admin 控制器中的Edit action 方法。

清单 28-29:Controllers 文件夹下的 AdminController.cs 文件,添加 Edit Actions

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Users.Models;
using System.Threading.Tasks;

namespace Users.Controllers
{
    public class AdminController : Controller
    {
        private UserManager<AppUser> userManager;

        private IUserValidator<AppUser> userValidator;
        private IPasswordValidator<AppUser> passwordValidator;
        private IPasswordHasher<AppUser> passwordHasher;

        public AdminController(UserManager<AppUser> usrMgr,
            IUserValidator<AppUser> userValid,
            IPasswordValidator<AppUser> passValid,
            IPasswordHasher<AppUser> passwordHash)
        {
            userManager = usrMgr;
            userValidator = userValid;
            passwordValidator = passValid;
            passwordHasher = passwordHash;
        }

        // 这里注意,原来的构造函数需要删除,用上面这个上替代
        // ...其它 actions 省略...

        public async Task<IActionResult> Edit(string id)
        {
            AppUser user = await userManager.FindByIdAsync(id);
            if (user != null)
            {
                return View(user);
            }
            else
            {
                return RedirectToAction("Index");
            }
        }

        [HttpPost]
        public async Task<IActionResult> Edit(string id, string email,
            string password)
        {
            AppUser user = await userManager.FindByIdAsync(id);
            if (user != null)
            {
                user.Email = email;
                IdentityResult validEmail
                    = await userValidator.ValidateAsync(userManager, user);
                if (!validEmail.Succeeded)
                {
                    AddErrorsFromResult(validEmail);
                }
                IdentityResult validPass = null;
                if (!string.IsNullOrEmpty(password))
                {
                    validPass = await passwordValidator.ValidateAsync(userManager,
                        user, password);
                    if (validPass.Succeeded)
                    {
                        user.PasswordHash = passwordHasher.HashPassword(user,
                        password);
                    }
                    else
                    {
                        AddErrorsFromResult(validPass);
                    }
                }
                if ((validEmail.Succeeded && validPass == null)
                    || (validEmail.Succeeded
                    && password != string.Empty && validPass.Succeeded))
                {
                    IdentityResult result = await userManager.UpdateAsync(user);
                    if (result.Succeeded)
                    {
                        return RedirectToAction("Index");
                    }
                    else
                    {
                        AddErrorsFromResult(result);
                    }
                }
            }
            else
            {
                ModelState.AddModelError("", "User Not Found");
            }
            return View(user);
        }

        private void AddErrorsFromResult(IdentityResult result)
        {
            foreach (IdentityError error in result.Errors)
            {
                ModelState.AddModelError("", error.Description);
            }
        }
    }
}

GET 请求所针对的Edit action 使用嵌入在Index视图中的 ID 字符串来调用FindByIdAsync方法来获取表示用户的AppUser对象。

更复杂的实现接收POST请求,并为用户 ID、新的电子邮件地址和密码设置参数。我必须执行几个 task 才能完成编辑操作。

第一个 task 是验证我收到的值。目前我正在使用一个简单的用户对象 —— 尽管我将在第30章向您展示如何定制为用户存储的数据 —— 但即便如此,我仍然需要验证用户数据,以确保不会违反《验证用户详细信息》和《验证密码》中定义的自定义策略。首先验证电子邮件地址,我喜欢这样做:

...
user.Email = email;
IdentityResult validEmail = await userValidator.ValidateAsync(userManager, user);
if (!validEmail.Succeeded) 
{
    AddErrorsFromResult(validEmail);
}
...

我为IUserValidator<AppUser>对象向控制器构造函数添加了一个依赖项,以便验证新的电子邮件地址。注意,在执行验证之前,我必须更改Email属性的值,因为ValidateAsync方法只接受用户类的实例。

下一步是,如果已提供密码,则更改密码。ASP.NET Core Identity 存储密码哈希值,而不是密码本身。这是为了防止密码被盗。我的下一步是使用经过验证的密码并生成将存储在数据库中的哈希代码,以便可以对用户进行身份验证,我在第29章中演示了这一点。

密码通过IPasswordHasher<AppUser>接口的实现转换为哈希值,该接口是通过声明将通过依赖注入获取的构造函数参数来获得的。IPasswordHasher接口定义了HashPassword方法,该方法接受一个字符串参数并返回其哈希值,如下所示:

...
if (!string.IsNullOrEmpty(password)) 
{
    validPass = await passwordValidator.ValidateAsync(userManager, user, password);
    if (validPass.Succeeded) 
    {
        user.PasswordHash = passwordHasher.HashPassword(user, password);
    } 
    else 
    {
        AddErrorsFromResult(validPass);
    }
}
...

在调用UpdateAsync方法之前,对用户类的更改不会存储在数据库中,如下所示:

...
if ((validEmail.Succeeded && validPass == null) || (validEmail.Succeeded
        && password != string.Empty && validPass.Succeeded)) 
{
    IdentityResult result = await userManager.UpdateAsync(user);
    if (result.Succeeded) 
    {
        return RedirectToAction("Index");
    } 
    else 
    {
        AddErrorsFromResult(result);
    }
}
...

创建视图

最后一个组件是将显示用户当前值并允许向控制器提交新值的视图。清单28-30显示了我在 Views/Admin 文件夹中创建的 Edit.cshtml 文件的内容。

清单 28-30:Views/Admin 文件夹下的 Edit.cshtml 文件的内容

@model AppUser

<div class="bg-primary m-1 p-1"><h4>Edit User</h4></div>

<div asp-validation-summary="All" class="text-danger"></div>

<form asp-action="Edit" method="post">
    <div class="form-group">
        <label asp-for="Id"></label>
        <input asp-for="Id" class="form-control" disabled />
    </div>
    <div class="form-group">
        <label asp-for="Email"></label>
        <input asp-for="Email" class="form-control" />
    </div>
    <div class="form-group">
        <label for="password">Password</label>
        <input name="password" class="form-control" />
    </div>
    <button type="submit" class="btn btn-primary">Save</button>
    <a asp-action="Index" class="btn btn-secondary">Cancel</a>
</form>

此视图将不能更改的用户 ID 显示为静态文本,并提供用于编辑电子邮件地址和密码的表单,如图28-13所示。注意,我没有为密码元素使用标签助手,因为用户类不包含密码信息,只有哈希值存储在数据库中。

最后的更改是注释掉Startup类中的用户验证设置,以便使用用户名的默认字符,如清单28-31所示。由于数据库中的某些帐户是在我更改验证设置之前创建的,因此您将无法编辑它们,因为用户名不会通过验证。而且,由于在验证电子邮件地址时,验证将应用于整个用户对象,因此结果是一个不能更改的用户帐户。

清单 28-31:Users 文件夹下的 Startup.cs 文件,禁用自定义验证设置

...
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IPasswordValidator<AppUser>,
        CustomPasswordValidator>();
    services.AddTransient<IUserValidator<AppUser>,
        CustomUserValidator>();

    services.AddDbContext<AppIdentityDbContext>(options =>
        options.UseSqlServer(
            Configuration["Data:SportStoreIdentity:ConnectionString"]));

    services.AddIdentity<AppUser, IdentityRole>(opts => {
        opts.User.RequireUniqueEmail = true;
        //opts.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz";
        opts.Password.RequiredLength = 6;
        opts.Password.RequireNonAlphanumeric = false;
        opts.Password.RequireLowercase = false;
        opts.Password.RequireUppercase = false;
        opts.Password.RequireDigit = false;
    }).AddEntityFrameworkStores<AppIdentityDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc();
}
...

要测试编辑特性,请运行应用程序,请求 /Admin URL,然后单击其中一个【Edit】按钮。更改电子邮件地址或输入新密码(或两者都输入),然后单击【Save】按钮更新数据库并返回 /Admin URL。

图28-13 编辑用户帐户

总结

本章我向您展示了如何创建使用 ASP.NET Coer Identity 所需的配置和类,并演示了如何将它们应用于创建用户管理工具。在下一章中,我将向您展示如何使用 ASP.NET Coer Identity 执行身份验证和授权。

;

© 2018 - IOT小分队文章发布系统 v0.3